// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/touch_selection/longpress_drag_selector.h"

#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/test/motion_event_test_utils.h"

using ui::test::MockMotionEvent;

namespace ui {
namespace {

    const double kSlop = 10.;

} // namespace

class LongPressDragSelectorTest : public testing::Test,
                                  public LongPressDragSelectorClient {
public:
    LongPressDragSelectorTest()
        : dragging_(false)
        , active_state_changed_(false)
    {
    }

    ~LongPressDragSelectorTest() override { }

    void SetSelection(const gfx::PointF& start, const gfx::PointF& end)
    {
        selection_start_ = start;
        selection_end_ = end;
    }

    bool GetAndResetActiveStateChanged()
    {
        bool active_state_changed = active_state_changed_;
        active_state_changed_ = false;
        return active_state_changed;
    }

    bool IsDragging() const { return dragging_; }
    const gfx::PointF& DragPosition() const { return drag_position_; }

    // LongPressDragSelectorClient implementation.
    void OnDragBegin(const TouchSelectionDraggable& handler,
        const gfx::PointF& drag_position) override
    {
        dragging_ = true;
        drag_position_ = drag_position;
    }

    void OnDragUpdate(const TouchSelectionDraggable& handler,
        const gfx::PointF& drag_position) override
    {
        drag_position_ = drag_position;
    }

    void OnDragEnd(const TouchSelectionDraggable& handler) override
    {
        dragging_ = false;
    }

    bool IsWithinTapSlop(const gfx::Vector2dF& delta) const override
    {
        return delta.LengthSquared() < (kSlop * kSlop);
    }

    void OnLongPressDragActiveStateChanged() override
    {
        active_state_changed_ = true;
    }

    gfx::PointF GetSelectionStart() const override { return selection_start_; }

    gfx::PointF GetSelectionEnd() const override { return selection_end_; }

private:
    bool dragging_;
    bool active_state_changed_;
    gfx::PointF drag_position_;

    gfx::PointF selection_start_;
    gfx::PointF selection_end_;
};

TEST_F(LongPressDragSelectorTest, BasicDrag)
{
    LongPressDragSelector selector(this);
    MockMotionEvent event;

    // Start a touch sequence.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.PressPoint(0, 0)));
    EXPECT_FALSE(GetAndResetActiveStateChanged());

    // Activate a longpress-triggered selection.
    gfx::PointF selection_start(0, 10);
    gfx::PointF selection_end(10, 10);
    selector.OnLongPressEvent(event.GetEventTime(), gfx::PointF());
    EXPECT_FALSE(GetAndResetActiveStateChanged());

    // Motion should not be consumed until a selection is detected.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, 0)));
    SetSelection(selection_start, selection_end);
    selector.OnSelectionActivated();
    EXPECT_TRUE(GetAndResetActiveStateChanged());
    EXPECT_FALSE(IsDragging());

    // Initiate drag motion.  Note that the first move event after activation is
    // used to initialize the drag start anchor.
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, 0)));
    EXPECT_FALSE(IsDragging());

    // The first slop exceeding motion will start the drag. As the motion is
    // downward, the end selection point should be moved.
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop)));
    EXPECT_TRUE(IsDragging());
    EXPECT_EQ(selection_end, DragPosition());

    // Subsequent motion will extend the selection.
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop * 2)));
    EXPECT_TRUE(IsDragging());
    EXPECT_EQ(selection_end + gfx::Vector2dF(0, kSlop), DragPosition());
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop * 3)));
    EXPECT_TRUE(IsDragging());
    EXPECT_EQ(selection_end + gfx::Vector2dF(0, kSlop * 2), DragPosition());

    // Release the touch sequence, ending the drag. The selector will never
    // consume the start/end events, only move events after a longpress.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.ReleasePoint()));
    EXPECT_FALSE(IsDragging());
    EXPECT_TRUE(GetAndResetActiveStateChanged());
}

TEST_F(LongPressDragSelectorTest, BasicReverseDrag)
{
    LongPressDragSelector selector(this);
    MockMotionEvent event;

    // Start a touch sequence.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.PressPoint(0, 0)));
    EXPECT_FALSE(GetAndResetActiveStateChanged());

    // Activate a longpress-triggered selection.
    gfx::PointF selection_start(0, 10);
    gfx::PointF selection_end(10, 10);
    selector.OnLongPressEvent(event.GetEventTime(), gfx::PointF());
    EXPECT_FALSE(GetAndResetActiveStateChanged());
    SetSelection(selection_start, selection_end);
    selector.OnSelectionActivated();
    EXPECT_TRUE(GetAndResetActiveStateChanged());
    EXPECT_FALSE(IsDragging());

    // Initiate drag motion.
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 5, 0)));
    EXPECT_FALSE(IsDragging());

    // As the initial motion is leftward, toward the selection start, the
    // selection start should be the drag point.
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, -kSlop, 0)));
    EXPECT_TRUE(IsDragging());
    EXPECT_EQ(selection_start, DragPosition());

    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, -kSlop)));
    EXPECT_TRUE(IsDragging());
    EXPECT_EQ(selection_start + gfx::Vector2dF(kSlop, -kSlop), DragPosition());

    // Release the touch sequence, ending the drag.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.ReleasePoint()));
    EXPECT_FALSE(IsDragging());
    EXPECT_TRUE(GetAndResetActiveStateChanged());
}

TEST_F(LongPressDragSelectorTest, NoActiveTouch)
{
    LongPressDragSelector selector(this);
    MockMotionEvent event;

    // Activate a longpress-triggered selection.
    gfx::PointF selection_start(0, 10);
    gfx::PointF selection_end(10, 10);
    selector.OnLongPressEvent(event.GetEventTime(), gfx::PointF());
    SetSelection(selection_start, selection_end);
    selector.OnSelectionActivated();
    EXPECT_FALSE(GetAndResetActiveStateChanged());
    EXPECT_FALSE(IsDragging());

    // Start a new touch sequence; it shouldn't initiate selection drag as there
    // was no active touch sequence when the longpress selection started.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.PressPoint(0, 0)));
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, 0)));
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop)));
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop * 2)));
    EXPECT_FALSE(IsDragging());
    EXPECT_EQ(gfx::PointF(), DragPosition());
}

TEST_F(LongPressDragSelectorTest, NoLongPress)
{
    LongPressDragSelector selector(this);
    MockMotionEvent event;

    // Start a touch sequence.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.PressPoint(0, 0)));
    EXPECT_FALSE(GetAndResetActiveStateChanged());

    // Activate a selection without a preceding longpress.
    gfx::PointF selection_start(0, 10);
    gfx::PointF selection_end(10, 10);
    SetSelection(selection_start, selection_end);
    selector.OnSelectionActivated();
    EXPECT_FALSE(GetAndResetActiveStateChanged());
    EXPECT_FALSE(IsDragging());

    // Touch movement should not initiate selection drag.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, 0)));
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop)));
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop * 2)));
    EXPECT_FALSE(IsDragging());
    EXPECT_EQ(gfx::PointF(), DragPosition());
}

TEST_F(LongPressDragSelectorTest, NoValidLongPress)
{
    LongPressDragSelector selector(this);
    MockMotionEvent event;

    // Start a touch sequence.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.PressPoint(0, 0)));
    EXPECT_FALSE(GetAndResetActiveStateChanged());

    gfx::PointF selection_start(0, 10);
    gfx::PointF selection_end(10, 10);
    SetSelection(selection_start, selection_end);

    // Activate a longpress-triggered selection, but at a time before the current
    // touch down event.
    selector.OnLongPressEvent(
        event.GetEventTime() - base::TimeDelta::FromSeconds(1), gfx::PointF());
    selector.OnSelectionActivated();
    EXPECT_FALSE(GetAndResetActiveStateChanged());
    EXPECT_FALSE(IsDragging());

    // Activate a longpress-triggered selection, but at a place different than the
    // current touch down event.
    selector.OnLongPressEvent(event.GetEventTime(), gfx::PointF(kSlop, 0));
    selector.OnSelectionActivated();
    EXPECT_FALSE(GetAndResetActiveStateChanged());
    EXPECT_FALSE(IsDragging());

    // Touch movement should not initiate selection drag.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, 0)));
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop)));
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop * 2)));
    EXPECT_FALSE(IsDragging());
    EXPECT_EQ(gfx::PointF(), DragPosition());
}

TEST_F(LongPressDragSelectorTest, NoSelection)
{
    LongPressDragSelector selector(this);
    MockMotionEvent event;

    // Start a touch sequence.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.PressPoint(0, 0)));
    EXPECT_FALSE(GetAndResetActiveStateChanged());

    // Trigger a longpress.
    selector.OnLongPressEvent(event.GetEventTime(), gfx::PointF());
    EXPECT_FALSE(GetAndResetActiveStateChanged());
    EXPECT_FALSE(IsDragging());

    // Touch movement should not initiate selection drag, as there is no active
    // selection.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, 0)));
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop)));
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop * 2)));
    EXPECT_FALSE(IsDragging());
    EXPECT_EQ(gfx::PointF(), DragPosition());

    EXPECT_FALSE(selector.WillHandleTouchEvent(event.ReleasePoint()));
}

TEST_F(LongPressDragSelectorTest, NoDragMotion)
{
    LongPressDragSelector selector(this);
    MockMotionEvent event;

    // Start a touch sequence.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.PressPoint(0, 0)));
    EXPECT_FALSE(GetAndResetActiveStateChanged());

    // Activate a longpress-triggered selection.
    selector.OnLongPressEvent(event.GetEventTime(), gfx::PointF());
    EXPECT_FALSE(GetAndResetActiveStateChanged());
    gfx::PointF selection_start(0, 10);
    gfx::PointF selection_end(10, 10);
    SetSelection(selection_start, selection_end);
    selector.OnSelectionActivated();
    EXPECT_TRUE(GetAndResetActiveStateChanged());
    EXPECT_FALSE(IsDragging());

    // Touch movement within the slop region should not initiate selection drag.
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, 0)));
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop / 2)));
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, -kSlop / 2)));
    EXPECT_FALSE(IsDragging());
    EXPECT_EQ(gfx::PointF(), DragPosition());

    EXPECT_FALSE(selector.WillHandleTouchEvent(event.ReleasePoint()));
    EXPECT_TRUE(GetAndResetActiveStateChanged());
}

TEST_F(LongPressDragSelectorTest, SelectionDeactivated)
{
    LongPressDragSelector selector(this);
    MockMotionEvent event;

    // Start a touch sequence.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.PressPoint(0, 0)));
    EXPECT_FALSE(GetAndResetActiveStateChanged());

    // Activate a longpress-triggered selection.
    selector.OnLongPressEvent(event.GetEventTime(), gfx::PointF());
    EXPECT_FALSE(GetAndResetActiveStateChanged());
    gfx::PointF selection_start(0, 10);
    gfx::PointF selection_end(10, 10);
    SetSelection(selection_start, selection_end);
    selector.OnSelectionActivated();
    EXPECT_TRUE(GetAndResetActiveStateChanged());
    EXPECT_FALSE(IsDragging());

    // Start a drag selection.
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, 0)));
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop)));
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop * 2)));
    EXPECT_TRUE(IsDragging());

    // Clearing the selection should force an end to the drag.
    selector.OnSelectionDeactivated();
    EXPECT_TRUE(GetAndResetActiveStateChanged());
    EXPECT_FALSE(IsDragging());

    // Subsequent motion should not be consumed.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, 0)));
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop)));
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.MovePoint(0, 0, kSlop * 2)));
    EXPECT_FALSE(IsDragging());
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.ReleasePoint()));
}

TEST_F(LongPressDragSelectorTest, DragFast)
{
    LongPressDragSelector selector(this);
    MockMotionEvent event;

    // Start a touch sequence.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.PressPoint(0, 0)));
    EXPECT_FALSE(GetAndResetActiveStateChanged());

    // Activate a longpress-triggered selection.
    gfx::PointF selection_start(0, 10);
    gfx::PointF selection_end(10, 10);
    selector.OnLongPressEvent(event.GetEventTime(), gfx::PointF());
    EXPECT_FALSE(GetAndResetActiveStateChanged());
    SetSelection(selection_start, selection_end);
    selector.OnSelectionActivated();
    EXPECT_TRUE(GetAndResetActiveStateChanged());
    EXPECT_FALSE(IsDragging());

    // Initiate drag motion.
    EXPECT_TRUE(selector.WillHandleTouchEvent(event.MovePoint(0, 15, 5)));
    EXPECT_FALSE(IsDragging());

    // As the initial motion exceeds both endpoints, the closer bound should
    // be used for dragging, in this case the selection end.
    EXPECT_TRUE(selector.WillHandleTouchEvent(
        event.MovePoint(0, 15.f + kSlop * 2.f, 5.f + kSlop)));
    EXPECT_TRUE(IsDragging());
    EXPECT_EQ(selection_end, DragPosition());

    // Release the touch sequence, ending the drag.
    EXPECT_FALSE(selector.WillHandleTouchEvent(event.ReleasePoint()));
    EXPECT_FALSE(IsDragging());
    EXPECT_TRUE(GetAndResetActiveStateChanged());
}

TEST_F(LongPressDragSelectorTest, ScrollAfterLongPress)
{
    LongPressDragSelector selector(this);
    MockMotionEvent event;
    gfx::PointF touch_point(0, 0);

    // Start a touch sequence.
    EXPECT_FALSE(selector.WillHandleTouchEvent(
        event.PressPoint(touch_point.x(), touch_point.y())));

    // Long-press and hold down.
    selector.OnLongPressEvent(event.GetEventTime(), touch_point);

    // Scroll the page. This should cancel long-press drag gesture.
    touch_point.Offset(0, 2 * kSlop);
    EXPECT_FALSE(selector.WillHandleTouchEvent(
        event.MovePoint(0, touch_point.x(), touch_point.y())));
    selector.OnScrollBeginEvent();

    // Now if the selection is activated, because long-press drag gesture was
    // canceled, active state of the long-press selector should not change.
    selector.OnSelectionActivated();
    EXPECT_FALSE(GetAndResetActiveStateChanged());

    // Release the touch sequence.
    selector.WillHandleTouchEvent(event.ReleasePoint());
}

} // namespace ui
